APPENDIX A
Previous Section Table of Contents Index Errata

Extending LScript: Linking With Native Code

The LScript core engine can link directly to shared libraries (Windows DLLs or UNIX Shared Libraries) to harness the power and speed of UDFs written in native machine code. More often than not, this native code will be created from C or C++ source. This functionality is very much like providing plug-in capabilities to...well...a plug-in.

In the remainder of this document, we will concentrate our discussions and efforts in describing shared libraries in terms of Microsoft Windows Dynamic Link Libraries (DLL). Creating shared libraries under UNIX versions of LScript can vary by platform, so we will concentrate upon the most popular LightWave 3D platform. Most of the techniques discussed can be applied equally to the UNIX method of generating a LScript-linkable shared library.

Under Microsoft Windows, there are several steps required to generate a DLL library capable of linking with your LScript scripts. First, you must become familiar with the LScript Interface Mechanism. This mechanism, although sparse, allows you to interact directly with an executing LScript. It is defined in the 'lscript.h' header file, which must be included in each Windows DLL source file that you compile.

Several items are defined in the 'lscript.h' header file for your use in writing your library. First, the data types that can be passed between an LScript and a DLL function are provided as manifest constants. Although some of the more complex LScript data types are not currently available to a DLL function, quite a lot can be done with the data types that are.

        #define LSINTEGER   1
        #define LSNUMBER    2
        #define LSSTRING    3
        #define LSVECTOR    4
        #define LSFILE      5
        #define LSNIL       99
The next item defined is a structure used to contain the data that is passed between an LScript and the DLL function:

        typedef struct _ls_var
        {
            int     type;

            union {
                double      number;
                const char *string;
                double      vector[3];
            } data;
        } LS_VAR;
The 'type' member holds one of the data types defined at the head of the file (LSINTEGER, LSNUMBER, etc.). Based upon the 'type', you can extract data from one of the 'data' union members 'number', 'string' or 'vector'. The 'number' member of the union is used to contain both REAL and INTEGER values. For example, if the 'type' of the data is LSINTEGER, then a simple cast of the 'number' member will extract the integer value:

         x = (int)...data.member;
The next entry in the header is the function prototype to which all DLL functions wishing to interact with an LScript must adhere. Your DLL functions will not use this typedef directly, rather it is include as a guide while creating your functions. The LScript core engine will invoke your DLL function using this prototype:

        typedef LS_VAR * (*DLLfunc)(int *count,LS_VAR *,LSFunc *);
Because LScript is itself a shared library, it utilizes the resources of the host process that has loaded it (i.e., LightWave 3D). When LScript allocates memory or opens files, it does so by using LightWave 3D's available resources. For this reason, LScript attempts to be a courteous guest in LightWave's house by cleaning up after itself, thus avoiding the introduction of memory leaks into Layout. To accomplish this, LScript tracks all memory allocations that take place during the execution of a script so that, when it terminates, it can free any resources that were not properly released during the script's execution.

LScript expects a guest in its house to be no less courteous. To that end, LScript extends to its guests access to the internal functions that it uses to manage these critical allocations. You are encouraged to take advantage of these functions when your shared library function needs to allocate resources (either memory or files). This is not, however, a license to be sloppy in your handling of these resources. You should maintain a discipline of explicitly allocating and freeing the resources you use, but you should do so using these exported internal LScript functions instead of utilities provided by your standard libraries. In this way, should you accidentally overlook an explicit resource release, LScript can catch it upon termination.

The next entries in the lscript.h header file is a structure definition that contains pointers to, among other things, each of these internal resource-management functions exported by LScript:

        typedef struct _lsfunc
        {
            char    * (*strdup)(const char *);
            void    * (*malloc)(int);
            void      (*free)(void *);

            void      (*info)(const char *);
            void      (*error)(const char *);
        } LSFunc;
If you refer back to the function prototype defined earlier, you will notice that the last parameter to each shared library function is a pointer to a structure of this type, LSFunc. Using this structure pointer, you can access the function pointers contained within to perform your resource management:

        LS_VAR *dllfunc(int count,LS_VAR *parameters,LSFunc *func)
        {
            ...
            // allocate an array of 5 LS_VARs
            vars = (LS_VAR *)(*func->malloc)(sizeof(LS_VAR) * 5);
            ...
            // release our allocations...
            (*func->free)(vars);
            ...
        }

Displaying Messages

There are two function pointers contained in the LSFunc structure that are similar in name to LScript functions, specifically info() and error(). These function pointers are quite similar to their LScript counterparts, and offer a shared library the means to post informational or error messages to the LightWave 3D user via LScript's internal message-handling mechanism.

These functions are invoked in the same fashion as other function pointers listed in the structure, but they will produce a visible result to the user:

            ...
            (*func->error)("severe error: bad parameter");
            ...
There are two important facts regarding these functions that you need to be aware of. First, the message that you pass to LScript is not the final message that will be displayed. LScript places information at the beginning of your message (and potentially at the end) before it displays the message to the user. For this reason, you should probably follow the guideline that your message should not begin with a capitalized word, and it should never assume that it is all the information that the user will see. For instance, the output from the previous example might appear to the user as:

                  Line #45, severe error: bad parameter
Second, any invocation of the error() function will set an internal flag that will cause LScript to halt its execution of the current script when the shared library function terminates. Therefore, error() is intended to be used only when a severe error is encountered within a shared library function that will cause subsequent errors to arise later in the script.

Building A Shared Library

Your next step in creating an LScript-linkable shared library is to define your DLL functions (making sure that you adhere to the guidelines set forth previously). Let's say that, under the LightWave Windows platform, we need a way to acquire the volume serial number of a logical drive. This might be accomplished from an LScript, but it would be very awkward (and slow). The solution is to write a new "built-in" function to accomplish the same task. Here, then, is a C function that will retrieve the volume serial number of the current logical drive with the help of some functions from the Windows Win32 API:

     char *getVolumeSerialNumber(void)
     {
         char    tmpPath[MAX_PATH + 1];
         char    tmpFile[MAX_PATH + 1];
         static  char volumeSN[50];
         BY_HANDLE_FILE_INFORMATION fileInfo;
         HANDLE  hFile;

         GetTempPath(MAX_PATH,tmpPath);
         GetTempFileName(tmpPath,"LScript",0,tmpFile);

         hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE,
                            0,NULL,OPEN_ALWAYS,
                            FILE_ATTRIBUTE_TEMPORARY,NULL);

         GetFileInformationByHandle(hFile,&fileInfo);
         sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber),
                                      LOWORD(fileInfo.dwVolumeSerialNumber));

         CloseHandle(hFile);
         DeleteFile(tmpFile);

         return(&volumeSN);
     }
Now, in its current state, we cannot use this function directly in an LScript. We must modify it to satisfy the LScript linking conditions. The modifications, however, will be comparatively minor.

First, we must change the header of the function to exactly match the typedef set forth in the 'lscript.h' header. Thus,

     char *getVolumeSerialNumber(void)
becomes

     LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars,LSFunc *func)
Next, we must have a value to return to the LScript. In the original function, we simply returned a character pointer to a static area. This is not sufficient for interfacing with LScript. We must return one or more LS_VAR structures, each holding a single item of LScript-readable data. In the case of this particular function, we will be returning the same character string wrapped in a LS_VAR structure.

We will declare our own private LS_VAR variable in the function. We must also make sure that the variable itself will exist after the function has returned, so we will declare it as "static":

            ...
            static  LS_VAR var;
            ...
Likewise, the character string we are returning must also continue to exist after the function returns (otherwise, we will be flirting with disaster when LScript tries to access "released" memory). Fortunately, we already have a "permanent" area, defined in the original function, where the character string can reside without fear of violating memory:

            ...
            static  char volumeSN[50];
            ...
Now, because we have altered the parameters that the function accepts, we are no longer able to rely on the compiler to detect an incorrect parameter count at compile time. We must therefore check this parameter count at runtime. It is up to the shared library function to ensure that the count of parameters passed into it by LScript is correct:

            if(*count != 0)      // we take no arguments
            {
                (*func->error)("invalid parameter count to DLL function ... ");
                return(NULL);
            }
In the case of the getVolumeSerialNumber() function, it takes no arguments (i.e., "void"). If any are passed to it, this should be an error.

The main body of the function remains largely unchanged. Once we have ascertained the volume serial number, we then must package it into a format that LScript can digest. In the original function, we simply returned a character pointer to our static string area:

            return(&volumeSN);
However, for LScript to properly handle the data, it must be wrapped by our LS_VAR structure, and we must indicate to the LScript engine not only their types, but how many data items we are returning. The new exit code of the function looks like this:

            var.type = LSSTRING;
            var.data.string = volumeSN;

            *count = 1;   // how many data items to expect

            return(&var);
        }
Here, then, is the complete LScript shared library version of the getVolumeSerialNumber() function:

        #include <stdio.h>
        #include <windows.h>

        #include "lscript.h"

        LS_VAR * getVolumeSerialNumber(int *count,LS_VAR *vars)
        {
            char    tmpPath[MAX_PATH + 1];
            char    tmpFile[MAX_PATH + 1];
            static  char volumeSN[50];
            static  LS_VAR var;
            BY_HANDLE_FILE_INFORMATION fileInfo;
            HANDLE  hFile;

            if(*count != 0)      // we take no arguments
                return(NULL);

            GetTempPath(MAX_PATH,tmpPath);
            GetTempFileName(tmpPath,"LScript",0,tmpFile);

            hFile = CreateFile(tmpFile,GENERIC_READ | GENERIC_WRITE,
                               0,NULL,OPEN_ALWAYS,
                               FILE_ATTRIBUTE_TEMPORARY,NULL);

            GetFileInformationByHandle(hFile,&fileInfo);
            sprintf(volumeSN,"%.4X-%.4X",HIWORD(fileInfo.dwVolumeSerialNumber),
                                         LOWORD(fileInfo.dwVolumeSerialNumber));

            CloseHandle(hFile);
            DeleteFile(tmpFile);

            var.type = LSSTRING;
            var.data.string = volumeSN;

            *count = 1;

            return(&var);
        }

Exporting Functions To LScript

When you are ready to generate your DLL (perhaps after adding more functions), you must create a Definition file. The Definition file, among other things, tells the compiler which functions in the library should be visible (exported) to processes that use it. This is a very important point: if your functions are not exported through the use of this Definition file, then any references contained in your LScripts will go unresolved, and errors will occur.

A sample Definition file for the DLL function getVolumeSerialNumber() might look like:

        LIBRARY  test
        EXPORTS
            getVolumeSerialNumber
You can save this definition file under any name you wish, as long as you use that name when you compile the DLL library. In the case of this example, we have named it 'dll.def.' If and as you add functions to your DLL library, you should add their names under the "EXPORTS" section of the Definition file before you re-compile.

Using A Makefile

Next in the few remaining steps is the creation of a makefile for your DLL library. Makefile creation is as much an art as it is a science, and a discussion of creating makefiles is quite beyond the scope of this document. Instead, we provide here a functional makefile that you can use directly to compile the sample DLL source file presented in this section, and as a template for designing other makefiles. This makefile is intended to be used with the Microsoft Visual C++ development system.

        CFMACH  = -D_X86_=1

        CFLAGS  = -c -W3 $(CFMACH) -DWIN32 -D_WIN32 /O2 /I . $(OPT)
        LFLAGS  = -dll -def:.\dll.def

        OBJS = test.obj

        LLIBS   = libc.lib kernel32.lib

        .cpp.dll:
            cl $(CFLAGS) $*.cpp

        .c.dll:
            cl $(CFLAGS) $*.c

        all :  test.dll

        test.dll:    $(OBJS)
            link $(LFLAGS) -out:test.dll $(OBJS) $(LLIBS)

        test.obj    : test.c
If you are indeed using Microsoft Visual C++ as your development system, a simple invocation of 'nmake' will generate for you a shiny new DLL that you can now link directly into your LScript. Other development systems will likely require modification of the provide makefile, and possibly other files.

Using The Shared Library

The last step in this process of extending LScripts is to actually write an LScript that uses the DLL library. This can be accomplished using an LScript of only a few lines:

        library "test.dll";

        main
        {
            info("Your volume serial number is ",getVolumeSerialNumber());
        }
If a path is not specified in the 'library' command, then the LScript engine will scan the following locations in the following order to locate your DLL file:
           1. The current directory (which may or may not be the same
              location where the script resides)
           2. The Windows system directory (usually Windows\System or
              WinNT\System32)
           3. The LibraryPath entry in the LScript Configuration for this
              plug-in type (ls-if, ls-or, etc.)

Previous Section Table of Contents Index Errata
© 1996 Virtual Visions, Inc.
© 1997 NewTek, Inc.